﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using Antlr4.StringTemplate;
using System.Threading.Tasks;
using System.Collections.Concurrent;
using Antlr4.StringTemplate.Misc;

namespace NMigrate
{
	public class FileHandlersCollection : List<FileHandler>
	{
		public FileHandler FindHandler(FileInfo file)
		{
			return this.FirstOrDefault(o => o.CanExecute(file));
		}

		public T Find<T>()
		{
			return this.OfType<T>().FirstOrDefault();
		}

		public new T Add<T>(T fileHandler) where T : FileHandler
		{
			var already = Find<T>();
			if(already != null)
				Remove(already);
			base.Add(fileHandler);
			return fileHandler;
		}
	}

	public class MigrationResult
	{
		List<VersionMigrationResult> _FileResults = new List<VersionMigrationResult>();
		private string path;

		public MigrationResult(string path, int currentVersion)
		{
			CurrentVersion = currentVersion;
			this.path = path;
		}
		internal VersionMigrationResult AddNewVersion(int number)
		{
			VersionMigrationResult result = new VersionMigrationResult(number);
			result.MigrationResult = this;
			_FileResults.Add(result);
			return result;
		}

		public IEnumerable<VersionMigrationResult> VersionMigrationResults
		{
			get
			{
				return _FileResults;
			}
		}

		public Exception Error
		{
			get
			{
				return VersionMigrationResults.Select(v => v.Error).FirstOrDefault(e => e != null);
			}
		}
		public VersionMigrationResult FailedVersionMigration
		{
			get
			{
				return VersionMigrationResults.FirstOrDefault(v => v.Error != null);
			}
		}

		public override string ToString()
		{
			return "Migration " + path + " " + (Success ? "Success" : "Failed");
		}

		public int CurrentVersion
		{
			get;
			internal set;
		}

		public bool Success
		{
			get
			{
				return Error == null;
			}
		}

		public MigrationResult ThrowIfError()
		{
			foreach(var version in VersionMigrationResults)
				version.ThrowIfError();
			return this;
		}
	}

	public class FileMigrationResult
	{
		public FileHandler Handler
		{
			get;
			internal set;
		}
		public FileInfo File
		{
			get;
			internal set;
		}
		public DateTime EndDate
		{
			get;
			internal set;
		}

		public DateTime StartDate
		{
			get;
			internal set;
		}

		public bool Success
		{
			get
			{
				return Error == null;
			}
		}

		public Exception Error
		{
			get;
			internal set;
		}

		public FileInfo InitialScript
		{
			get;
			internal set;
		}

		public override string ToString()
		{
			return "File " + File.Name + " " + (Success ? "Success" : "Failed");
		}

		public FileMigrationResult ThrowIfError()
		{
			if(Error != null)
				throw new MigrationException(this);
			return this;
		}
	}
	[Serializable]
	public class TemplateException : Exception
	{
		public TemplateException()
		{
		}
		public TemplateException(string message)
			: base(message)
		{
		}
		public TemplateException(string message, Exception inner)
			: base(message, inner)
		{
		}
		protected TemplateException(
		  System.Runtime.Serialization.SerializationInfo info,
		  System.Runtime.Serialization.StreamingContext context)
			: base(info, context)
		{
		}


		public TemplateException(string message, TemplateMessage msg)
			: base(message, null)
		{
			_TemplateMessage = msg;
		}

		private readonly TemplateMessage _TemplateMessage;
		public TemplateMessage TemplateMessage
		{
			get
			{
				return _TemplateMessage;
			}
		}
	}
	[Serializable]
	public class MigrationException : Exception
	{
		public MigrationException(FileMigrationResult fileMigrationResult)
			: this(Message(fileMigrationResult), fileMigrationResult)
		{
		}

		public MigrationException(string message, FileMigrationResult fileMigration)
			: base(message, fileMigration.Error)
		{
			this._FileMigration = fileMigration;
		}

		public MigrationException(VersionMigrationResult versionMigrationResult, FileMigrationResult fileMigration)
			: this(Message(versionMigrationResult, fileMigration), fileMigration)
		{
			_VersionMigration = versionMigrationResult;
		}

		private static string Message(VersionMigrationResult versionMigrationResult, FileMigrationResult fileMigration)
		{
			return "The following error happened during passage to version " + versionMigrationResult.VersionNumber + " on " + fileMigration.InitialScript.Name + "\r\n" + fileMigration.Error.Message;
		}


		static string Message(FileMigrationResult fileMigrationResult)
		{
			return "The following error happened during execution of " + fileMigrationResult.InitialScript.Name + "\r\n" + fileMigrationResult.Error.Message;
		}



		private readonly VersionMigrationResult _VersionMigration;
		public VersionMigrationResult VersionMigration
		{
			get
			{
				return _VersionMigration;
			}
		}
		private readonly FileMigrationResult _FileMigration;
		public FileMigrationResult FileMigration
		{
			get
			{
				return _FileMigration;
			}
		}
	}

	public class VersionMigrationResult
	{

		List<FileMigrationResult> _FileResults = new List<FileMigrationResult>();
		private int number;

		public VersionMigrationResult(int versionNumber)
		{
			this._VersionNumber = versionNumber;
		}

		private readonly int _VersionNumber;
		public int VersionNumber
		{
			get
			{
				return _VersionNumber;
			}
		}

		public IEnumerable<FileMigrationResult> FileMigrationResults
		{
			get
			{
				return _FileResults;
			}
		}

		internal void Add(FileMigrationResult fileMigrationResult)
		{
			_FileResults.Add(fileMigrationResult);
		}

		public Exception Error
		{
			get
			{
				return FileMigrationResults.Select(v => v.Error).FirstOrDefault(e => e != null);
			}
		}

		public FileMigrationResult FailedFileMigration
		{
			get
			{
				return FileMigrationResults.FirstOrDefault(f => f.Error != null);
			}
		}

		public bool Success
		{
			get
			{
				return Error == null;
			}
		}
		public override string ToString()
		{
			return "Version " + VersionNumber + " " + (Success ? "Success" : "Failed");
		}

		public VersionMigrationResult ThrowIfError()
		{
			if(FailedFileMigration != null)
			{
				throw new MigrationException(this, FailedFileMigration);
			}
			return this;
		}

		public MigrationResult MigrationResult
		{
			get;
			internal set;
		}
	}

	public class Migration
	{
		class ErrorListener : ITemplateErrorListener
		{
			#region ITemplateErrorListener Members

			public void CompiletimeError(TemplateMessage msg)
			{
				throw new TemplateException("Compile error", msg);
			}

			public void IOError(TemplateMessage msg)
			{
				throw new TemplateException("IO Error", msg);
			}

			public void InternalError(TemplateMessage msg)
			{
				throw new TemplateException("internal error", msg);
			}

			public void RuntimeError(TemplateMessage msg)
			{
				throw new TemplateException("Error during templating, most likely access to a property thrown an exception, see TemplateMessage property", msg);
			}

			#endregion
		}
		public Migration()
		{
			RegisterDefaultHandlers();
		}

		private void RegisterDefaultHandlers()
		{
			FileHandlers.Add(new BatchFileHandler());
			FileHandlers.Add(new CsFileHandler());
			FileHandlers.Add(new ConfigFileHandler());
			FileHandlers.Add(new SqlFileHandler());
		}
		private readonly FileHandlersCollection _FileHandlers = new FileHandlersCollection();
		public FileHandlersCollection FileHandlers
		{
			get
			{
				return _FileHandlers;
			}
		}

		public string TempDirectory
		{
			get;
			set;
		}
		public Configuration Configuration
		{
			get;
			set;
		}
		public MigrationResult Migrate(string path)
		{
			return Migrate(path, 0);
		}
		public MigrationResult Migrate(string path, bool throwOnError)
		{
			return Migrate(path, 0, throwOnError);
		}
		public MigrationResult Migrate(string path, int currentVersion)
		{
			return Migrate(path, currentVersion, false);
		}
		public MigrationResult Migrate(string path, int currentVersion, bool throwOnError)
		{
			MigrationResult result = new MigrationResult(path, currentVersion);
			foreach(var fileGroup in new DirectoryInfo(path)
				.GetFiles()
				.Select(f => FileName.Parse(f))
				.Where(o => o != null)
				.Where(o => o.Number > currentVersion)
				.GroupBy(o => o.Number)
				.OrderBy(o => o.Key)
				)
			{
				var versionResult = result.AddNewVersion(fileGroup.Key);
				var tasks = fileGroup.Select(f => Task.Factory.StartNew<FileMigrationResult>((o) => MigrateFile(f.Extension, f.File, false), f.File)).ToArray();
				bool failed = false;
				foreach(var task in tasks)
				{
					task.Wait();
					versionResult.Add(task.Result);
					failed |= !task.Result.Success;
				}

				if(failed)
				{
					if(throwOnError)
						versionResult.ThrowIfError();
					return result;
				}
				else
					result.CurrentVersion = versionResult.VersionNumber;
			}
			return result;
		}
		public FileMigrationResult MigrateFile(string file)
		{
			var fileInfo = new FileInfo(file);
			return MigrateFile(fileInfo.Extension, fileInfo, true);
		}

		private FileMigrationResult MigrateFile(string extension, FileInfo fileInfo, bool throwExceptions)
		{
			FileMigrationResult fileMigrationResult = new FileMigrationResult();
			fileMigrationResult.InitialScript = fileInfo;
			fileMigrationResult.File = fileInfo;
			fileMigrationResult.StartDate = DateTime.Now;
			try
			{
				var handler = FileHandlers.FindHandler(fileInfo);
				if(handler == null)
					return fileMigrationResult;
				fileMigrationResult.Handler = handler;
				try
				{
					if(handler.IsTemplated(fileInfo))
					{
						var targetFile = Path.GetTempPath() + fileInfo.Name;
						Template template = new Template(File.ReadAllText(fileInfo.FullName), '$', '$');
						template.Add("o", Configuration == null ? null : Configuration.GetValue());

						StringWriter writer = new StringWriter();
						var errorListener = new ErrorListener();
						template.Write(writer, errorListener);
						File.WriteAllText(targetFile, writer.ToString());
						fileInfo = new FileInfo(targetFile);
						fileMigrationResult.File = fileInfo;
					}

					handler.Execute(fileInfo, Configuration ?? new NullConfiguration());
				}
				catch(Exception ex)
				{
					fileMigrationResult.Error = ex;
					if(throwExceptions)
						fileMigrationResult.ThrowIfError();
				}
			}
			finally
			{
				fileMigrationResult.EndDate = DateTime.Now;
			}
			return fileMigrationResult;
		}
	}
}
